Skip to main content

Javascript library functions

info

A ready to-go example is in this repository, Clone it to <AppData>/wljs-notebook/wljs_packages

git clone https://github.com/JerryI/wljs-plugin-example-1

and restart WLJS Notebook

Here is the simples example on what you can do with extensions. Why not to add ApexCharts? They looks beautiful and already animated. What we need

  • Kernel package, which implements ApexCharts[] symbols
  • Javascript bundle, which should include ApexCharts and bridge it with Wolfram Kernel using WLJS Functions

Summary what will be done

  • package for evaluation kernel, which adds a new symbol ApexCharts
  • Javascript module, which renders the content of ApexCharts expression

Preparations

Use wljs-plugin-template template and create a new repository. Then clone new repository to <AppData>/wljs-notebook/wljs_packages folder. For example

git clone https://github.com/JerryI/wljs-plugin-example-1

Then edit the content of package.json

package.json
{
"name": "wljs-plugin-example-1",
"version": "0.0.1",
"scripts": {
"build": "node --max-old-space-size=8192 ./node_modules/.bin/rollup --config rollup.config.mjs"
},
"description": "An example plugin for WLJS Notebook. Library functions",
"wljs-meta": {
"kernel": [
"src/Kernel.wl"
],
"js": "dist/kernel.js",
"minjs": "dist/kernel.min.js",
"priority": 5000,
"category": "Notebook Extensions"
},
"repository": {
"type": "git",
"url": "https://github.com/JerryI/wljs-plugin-example-1"
},
"dependencies": {
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-terser": "^0.4.4",
"rollup": "^3.21.6"
}
}

By the default a template implies, that we will use rollup.js for bundling. Run in the root directory of this package

npm i

And then install apexcharts

npm i apexcharts

Kernel package

Create a new file in src/Kernel.wl, which is going to be our package for evaluation kernel. Looking at ApexCharts API, it is easy to imagine the way how it can be used

ApexCharts[<|
"series" -> {44, 55, 67, 83},
"labels" -> {"Apples", "Oranges", "Bananas", "Berries"},
"chart" -> <|
"height" -> 350,
"type" -> "radialBar"
|>
|>]

or any other way, one can pre-transform the data and use intermediate symbols. Following the simplest path, we write to our Kernel file

src/Kernel.wl
BeginPackage["CoffeeLiqueur`Extensions`ApexCharts`"]

(* Public context *)

ApexCharts::usage = "ApexCharts[a_Association] constructor"

Begin["`Private`"]


End[]
EndPackage[]

We can put a few checks to ensure that input is an association

nonAssocHeadQ[_] = True
nonAssocHeadQ[_Association] = False

ApexCharts::notassoc = "Input is not an association"
ApexCharts[_?nonAssocHeadQ ] := (Message[ApexCharts::notassoc]; $Failed)

Now we can think about ApexCharts as if it was a new entity, and the interpretation of this entity will be our graphs. To draw actual graphs instead of a symbolic representation we need to define an output form. For that ViewBox comes in hand, which is provided in CoffeeLiqueur`Extensions`Boxes` context

BeginPackage["CoffeeLiqueur`Extensions`ApexCharts`", {
"CoffeeLiqueur`Extensions`Boxes`"
}]

(* Public context *)

...

(* Output forms *)

ApexCharts /: MakeBoxes[a: ApexCharts[_Association], StandardForm ] := With[{},
ViewBox[a, a]
]

First a will be an underlying expression (behind the graph), while second a is going to be rendered instead of it. However, if you try to evaluate this with defined output form, you get a similar error

ViewBox tries to execute the expression provided as the second argument in the browser, but ApexCharts does not exist there.

Javascript Library

Now we need to implement ApexCharts on the frontend. The idea is simple: take the provided data and using Apex API render a graph on the given DOM element

src/kernel.js
let ApexCharts;

core.ApexCharts = async (args, env) => {
if (!ApexCharts) ApexCharts = (await import('apexcharts')).default; //lazy loading

const options = await interpretate(args[0], env);
const chart = new ApexCharts(env.element, options);
chart.render();
}

Our association will be in options object after the interpretation.

tip

Implement dynamic imports (lazy) if possible

Now we need to bundle this

npm run build

After the restart, it should work with our extension like a charm

However, after several evaluation dead instances of ApexCharts will pile up as a garbage in Javascript memory. It is recommended to properly remove them. For that we need to identify each instance of ApexCharts and assign destructor function

src/kernel.js
let ApexCharts;

core.ApexCharts = async (args, env) => {
if (!ApexCharts) ApexCharts = (await import('apexcharts')).default; //lazy loading

const options = await interpretate(args[0], env);
const chart = new ApexCharts(env.element, options);
chart.render();

env.local.chart = chart;
}

core.ApexCharts.destroy = (args, env) => {
env.local.chart.destroy();
}

core.ApexCharts.virtual = true

Now we need to bundle this

npm run build

After the restart it should work with our extension like a charm

Polishing

We might do a few more tweaks. If our ApexCharts expression becomes too big, it might slow down the editor. For that reason we use Frontend Objects

BeginPackage["CoffeeLiqueur`Extensions`ApexCharts`", {
"CoffeeLiqueur`Extensions`Boxes`",
"CoffeeLiqueur`Extensions`FrontendObject`"
}]

(* Public context *)
ApexCharts /: MakeBoxes[a: ApexCharts[_Association], form: StandardForm ] := With[{o = CreateFrontEndObject[a]},
MakeBoxes[o, form]
] /; ByteCount[a] > 1024*4

Frontend objects has a special predefined StandardForm (and WLXForm), where it uses a reference to a Wolfram Expression stored separately.

Another improvement can be WLXForm, if we decide to use those charts on Slides

ApexCharts /: MakeBoxes[a: ApexCharts[_Association], form: WLXForm ] := With[{o = CreateFrontEndObject[a]},
MakeBoxes[o, form]
]
tip

Full source code can be found in this repository